Spring Boot - Bean 加载方式
介绍 Bean 的八种加载方式
配置文件 + <bean/> 标签
古老的记忆袭击,第一种方式就是给出 Bean 的类名,内部通过反射机制加载成 Class。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- xml 方式声明自己开发的 bean-->
<bean id="cat" class="Cat"/>
<bean class="Dog"/>
<!-- xml 方式声明第三方开发的 bean-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>配置文件扫描 + 注解定义 Bean
这里可以使用的注解有 @Component 以及三个衍生注解 @Service、@Controller、@Repository。
@Component("tom")
public class Cat {
}
@Service
public class Mouse {
}
@Component
public class DbConfig {
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}从前从前,是通过配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--指定扫描加载 Bean 的位置-->
<context:component-scan base-package="com.itheima.bean,com.itheima.config"/>
</beans>现在可以直接在启动类加启动注解 @SpringBootApplication:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(GeneratorApplication.class, args);
}
}注意的是该文件所在的目录层级优先级最高。
注解方式声明配置类
使用 @ComponentScan 扫描指定的包路径下的带有 @Component 以及三个衍生注解 @Service、@Controller、@Repository 的类。
@ComponentScan({"cn.youngkbt.bean","cn.youngkbt.config"})
public class SpringConfig {
@Bean
public DogFactoryBean dog(){
return new DogFactoryBean();
}
}值得一提的是,这里也使用了另一个注解 @Bean,将方法的结果作为 Bean 传入 Spring 容器,前提是 SpringConfig 被加载为 Bean。
再值得一提的一个知识是,DogFactoryBean 类使用了 FactoryBean 接口。
Spring 提供了一个接口 FactoryBean,也可以用于声明 Bean,只不过实现了 FactoryBean 接口的类造出来的对象不是当前类的对象,而是 FactoryBean 接口泛型指定类型的对象。如下列,造出来的 Bean 并不是 DogFactoryBean,而是 Dog。
public class DogFactoryBean implements FactoryBean<Dog> {
@Override
public Dog getObject() throws Exception {
Dog d = new Dog();
//.........
return d;
}
@Override
public Class<?> getObjectType() {
return Dog.class;
}
@Override
public boolean isSingleton() {
return true;
}
}等价于:
@ComponentScan({"cn.youngkbt.bean","cn.youngkbt.config"})
public class SpringConfig {
@Bean
public Dog dog(){
return new Dog();
}
}有人说,注释中的代码直接写入 Dog 的构造方法不就行了吗?干嘛这么费劲转一圈,还写个类,还要实现接口,多麻烦啊。还真不一样,你可以理解为 Dog 是一个抽象后剥离的特别干净的模型,但是实际使用的时候必须进行一系列的初始化动作。只不过根据情况不同,初始化动作不同而已。如果写入 Dog,或许初始化动作 A 当前并不能满足你的需要,这个时候你就要做一个 DogB 的方案了。你就要做两个 Dog 类。当时使用 FactoryBean 接口就可以完美解决这个问题。
番外
导入 XML 格式配置的 Bean
由于早起开发的系统大部分都是采用 XML 的形式配置 Bean,现在的企业级开发基本上不用这种模式了。 但是如果你特别幸运,需要基于之前的系统进行二次开发,这就尴尬了。新开发的用注解格式,之前开发的是 XML 格式。这个时候可不是让你选择用哪种模式的,而是两种要同时使用。
Spring 提供了一个注解可以解决这个问题,@ImportResource,在配置类上直接写上要被融合的 XML 配置文件名即可,算的上一种兼容性解决方案。
@Configuration
@ImportResource("applicationContext.xml")
public class SpringConfig {
}这将会去 application.yml 根目录下读取 applicationContext.xml 文件。
proxyBeanMethods 属性
@Configuration 这个注解,当我们使用 AnnotationConfigApplicationContext 加载配置类的时候,配置类可以不添加这个注解。但是这个注解有一个更加强大的功能,它可以保障配置类中使用方法创建的 Bean 的唯一性。为 @Configuration 注解设置 proxyBeanMethods 属性值为 true 即可,此属性默认值为 true。
当 proxyBeanMethods 为 true,则为 Full 模式,反之为 Lite 模式。
/**
* 1、配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理 Bean 的方法
* Full(proxyBeanMethods = true)、【保证每个 @Bean 方法被调用多少次返回的组件都是单实例的】
* Lite(proxyBeanMethods = false)【每个 @Bean 方法被调用多少次返回的组件都是新创建的】
* 组件依赖必须使用 Full 模式默认。其他默认是否 Lite 模式
*/
@Configuration(proxyBeanMethods = true)
public class SpringConfig {
@Bean
public Cat cat(){
return new Cat();
}
}什么叫做保证 Bean 的唯一性呢?
首先我们知道 Configuration 修饰某个类后,该类里的方法带有 @Bean 注解后,那么 Spring 会将该方法的返回值作为 Bean 注入到 Spring 容器里。
proxyBeanMethods 为 true 时,我们使用 @Autowired 或其他方法自动注入类的时候,该类将从 Spring 容器里获取,也就是单例。
但是 proxyBeanMethods 为 false 时,我们每次注入的都是一个全新的类,也就是说,Spring 每次注入的时候都会执行 @Bean 修饰的方法,拿到返回值来执行注入。而不是从 Spring 容器找出已经存在的该类来注入。
因此,proxyBeanMethods 控制 Spring 是从容器获取存在的类还是调用对应的方法得到返回值来返回类。
使用 @Impore 注入 Bean
使用扫描的方式加载 Bean 是企业级开发中常见的 Bean 的加载方式,但是由于扫描的时候不仅可以加载到你要的东西,还有可能加载到各种各样的乱七八糟的东西。
比如你扫描了 cn.youngkbt.service包,后来因为业务需要,又扫描了 cn.youngkbt.dao 包,你发现 cn.youngkbt 包下面有 service 和 dao 这两个包,这就简单了,直接扫描 cn.youngkbt 就行了。但是万万没想到,十天后你加入了一个外部依赖包,里面也有 cn.youngkbt 包,这下就热闹了,该来的不该来的全来了。
所以我们需要一种精准制导的加载方式,使用 @Import 注解就可以解决你的问题。它可以加载所有的一切,只需要在注解的参数中写上加载的类对应的 .class 即可。
@Import({Dog.class, DbConfig.class}) // 当 SpringConfig 加载后,也会加载 Dog、DbConfig 类
@Configuration
public class SpringConfig {
}除了加载 Bean,还可以使用 @Import 注解加载配置类。其实本质上是一样的。
@Import(DogFactoryBean.class)
@Configuration
public class SpringConfig {
}@Import 在 自己被加载后,手动去加载别的类,当我们有顺序的加载一连串有顺序的类可以用到。
如果只是单纯想把一个类加载到容器,不会对类进行任何操作,则 @Import 也可以代替 @Bean
@Configuration
public class SpringConfig {
@Bean
public Cat cat(){
return new Cat();
}
}变成
@Configuration
@Import(Cat.class)
public class SpringConfig {
}编程形式注册 Bean
前面介绍的加载 Bean 的方式都是在容器启动阶段完成 Bean 的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载 Bean。通过这种方式可以实现编程式控制 Bean 的加载。
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
// 上下文容器对象已经初始化完毕后,手工加载 Bean
ctx.register(Mouse.class);
}
}实现 ImportSelector 接口类
实现 ImportSelector 接口的类可以设置加载的 Bean 的全路径类名,记得一点,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
// 各种条件的判定,判定完毕后,决定是否装载指定的bean
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
if(flag){
return new String[]{"cn.youngkbt.bean.Dog"};
}
return new String[]{"cn.youngkbt.bean.Cat"};
}
}例子中的 Metadata 是获取导入这个 Selector 的配置类的源信息。可以从源信息中获取到配置类上的一些信息,可以通过这个信息进行自定义的逻辑判断来决定添加什么 Bean。
注意的是 AnnotationMetadata 在 Spring Boot 源码被大量使用(如自动装配技术),它代表元数据,那么是谁的元数据呢?是谁导入这个类,那么 AnnotationMetadata 就记录谁的元数据,如:
@Import(MyImportSelector.class)
@XXX
public class SpringConfig {
}SpringConfig 类手动加载 MyImportSelector 类,那么 MyImportSelector 类的 AnnotationMetadata 就记录者 SpringConfig 的信息,就可以使用 AnnotationMetadata 获取 SpringConfig 的 XXX 注解等该类的基本信息。
用这种方式来加载 Bean 会更灵活,适用于加载的 Bean 需要通过一些条件判断后来决定是否加载的场景。
实现 ImportBeanDefinitionRegistrar 接口类
方式六中提供了给定类全路径类名控制 Bean 加载的形式, 其实 Bean 的加载不是一个简简单单的对象,Spring 中定义了一个叫做 BeanDefinition 的东西,它才是控制 Bean 初始化加载的核心。BeanDefinition 接口中给出了若干种方法,可以控制 Bean 的相关属性。说个最简单的,创建的对象是单例还是非单例,在 BeanDefinition 中定义了 scope 属性就可以控制这个。如果你感觉方式六没有给你开放出足够的对 Bean 的控制操作,那么方式七你值得拥有。
我们可以通过定义一个类,然后实现 ImportBeanDefinitionRegistrar 接口的方式定义 Bean:
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 加载 BookServiceImpl 类
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl.class).getBeanDefinition();
// 注册类,key 是类在 Spring 容器的 id
registry.registerBeanDefinition("bookService", beanDefinition);
}
}上面代码只需要知道 BookServiceImpl 是实际加载的 Bean,而 BeanDefinition 只是将 BookServiceImpl 进行封装,于是我们可以使用 BeanDefinition 的对象,针对封装的 BookServiceImpl 进行 Bean 加载控制,比如单例还是非单例:
beanDefinition.setScope(ConfigurableBeanFactory.SCOPE_SINGLETON) // 单例实现 BeanDefinitionRegistryPostProcessor 接口类
上述七种方式都是在容器初始化过程中进行 Bean 的加载或者声明,但是这里有一个 Bug。这么多种方式,它们之间如果有冲突怎么办?谁能有最终裁定权?
BeanDefinitionRegistryPostProcessor,看名字知道,BeanDefinition 意思是 Bean 定义,Registry 注册的意思,Post 后置,Processor 处理器,全称 Bean 定义后处理器,干啥的?在所有 Bean 注册都折腾完后,它是最后一道关卡,说白了,它说了算,所以它是最后一个运行的。
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 加载 BookServiceImpl 类
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl.class).getBeanDefinition();
// 注册类,key 是类在 Spring 容器的 id
registry.registerBeanDefinition("bookService", beanDefinition);
}
}这是最后一道关卡,不管前面怎么加载 BookServiceImpl,这里只要在加载 BookServiceImpl 的时候额外设置一些东西,那么最终在容器的 BookServiceImpl 就以这个为主。
文字无法理解,可以看视频:https://www.bilibili.com/video/BV15b4y1a7yG?p=143。
看 p143 - p152,如果想了解自动装配原理,则看到 p143 - p160。